package org.dicr.sys.linux;

import java.io.*;
import java.lang.ref.*;
import java.util.*;
import java.util.logging.*;

import org.dicr.sys.*;
import org.dicr.util.data.*;
import org.dicr.util.net.*;

/**
 * Файл статических имен.
 * <P>
 * Файл статических имен в Linux имеет стандартное имя <CODE>/etc/hosts</CODE>.
 * </P>
 * <P>
 * Формат файла следующий:
 * 
 * <PRE>
 * 
 * IP full_name short_name
 * 
 * </PRE>
 * 
 * Например:
 * 
 * <PRE>
 * 
 * 192.168.0.1 server.local server 193.201.206.2 iserv.euromb.com iserv
 * 
 * </PRE>
 * 
 * </P>
 * <P>
 * Данные кэшируются в памяти. При изменении записываются немедленно на диск.
 * </P>
 * <P>
 * Для управления доступом к физическому файлу разных потоков создается реестр
 * создагнных обьектов. Если обьект, связанный с данным файлом уже существует,
 * то возвращается уже существующий экземпляр, иначе создается новый.
 * </P>
 * 
 * @author Igor A Tarasov, &lt;java@dicr.org&gt;
 * @version 1.0
 */
public class HostsFile extends SystemFileHandler {

	private static final Logger log = Logger.getLogger(HostsFile.class.getName());

	/**
	 * Имя файла hosts по-умолчанию.
	 */
	public static final String DEFAULT_NAME = "/etc/hosts";

	private long fileTime = 0;

	private SoftReference<Map<Integer, HostsEntry>> dataRef = null;

	/**
	 * Конструктор.
	 * 
	 * @param fileName
	 *            String
	 */
	private HostsFile(String fileName) throws FileLockedException {
		super(fileName);
		this.fileTime = 0;
		this.dataRef = null;
	}

	/**
	 * Return singleton instance
	 * 
	 * @param fileName
	 *            file name
	 * @return instance of object
	 * @throws FileLockedException
	 *             file locked
	 */
	public static synchronized HostsFile getInstance(String fileName) throws FileLockedException {
		if (fileName == null) {
			throw new IllegalArgumentException("null file");
		}
		String absName = new File(fileName).getAbsolutePath();
		HostsFile hostsFile = null;
		SystemFileHandler handler = getHandler(fileName);
		if (handler != null) {
			if (handler instanceof HostsFile) {
				hostsFile = (HostsFile) handler;
			} else {
				throw new FileLockedException(absName);
			}
		} else {
			hostsFile = new HostsFile(absName);
		}
		return hostsFile;
	}

	/**
	 * Возвращает экземпляр обьекта.
	 * <P>
	 * Возвращает экземпляр обьекта, привязанного к файлу по умолчанию
	 * {@link #DEFAULT_NAME}. Если обьект еще не создавался, то создает новый.
	 * </P>
	 * 
	 * @return HostsFile экземпляр обьекта, привязанный к файлу по умолчанию.
	 * @throws FileLockedException
	 *             file locked
	 */
	public static synchronized HostsFile getInstance() throws FileLockedException {
		return getInstance(DEFAULT_NAME);
	}

	/**
	 * Возвращает данные файла.
	 * <P>
	 * Перечитывает файл при необходимости и возвращает данные их кэша.
	 * </P>
	 * 
	 * @return множество хостов.
	 * @throws IOException
	 *             ошибка чтения файла.
	 */
	protected synchronized Map<Integer, HostsEntry> getData() throws IOException {
		Map<Integer, HostsEntry> data = null;
		if (this.dataRef != null) {
			data = this.dataRef.get();
		}
		if (data != null && this.fileTime > 0 && new File(getFileName()).lastModified() <= this.fileTime) {
			return data;
		}
		data = new HashMap<>();
		BufferedReader in = null;
		try {
			String s = null;
			in = new BufferedReader(new FileReader(getFileName()));
			while (true) {
				s = in.readLine();
				if (s == null) {
					break;
				}
				s = StringUtils.normalize(s);
				if (s == null || s.startsWith("#")) {
					continue;
				}
				String fields[] = s.split("[\\s\\t]+");
				if (fields.length < 2) {
					log.warning("error line '" + s + "' in file: " + getFileName());
					continue;
				}
				int ip = 0;
				try {
					ip = IP.parse(fields[0]);
				} catch (IncorrectAddressException ex) {
					log.log(Level.WARNING, "error parsing line '" + s + "' of file: " + getFileName(), ex);
					continue;
				}
				ArrayList<String> names = new ArrayList<>();
				for (int i = 1; i < fields.length; i++) {
					names.add(fields[i]);
				}
				data.put(Integer.valueOf(ip), new HostsEntry(ip, names.toArray(new String[0])));
			}
			this.fileTime = new File(getFileName()).lastModified();
			this.dataRef = new SoftReference<>(data);
		} catch (FileNotFoundException ex) {
			log.fine("hosts file '" + getFileName() + "' not found");
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (Exception ex) {
					log.log(Level.WARNING, "error closing file '" + getFileName() + "'", ex);
				}
			}
		}
		return data;
	}

	/**
	 * Сохраняет данные файла.
	 * 
	 * @param data
	 *            HashMap данные для записи, ключи - Integer IP-адреса, значения
	 *            - Collection<String> имена хоста.
	 * @throws IOException
	 */
	protected synchronized void saveData(Map<Integer, HostsEntry> data) throws IOException {
		assert (data != null);
		PrintWriter ou = null;
		try {
			ou = new PrintWriter(new BufferedWriter(new FileWriter(getFileName())));
			Set<Integer> hostsSet = data.keySet();
			for (Integer ip : hostsSet) {
				ou.print(IP.toString(ip.intValue()));
				ou.print("\t");
				String[] names = data.get(ip).getNames();
				for (String name : names) {
					ou.print(name);
					ou.print("\t");
				}
				ou.println();
			}
			ou.flush();
			this.dataRef = new SoftReference<>(data);
			this.fileTime = new File(getFileName()).lastModified();
		} finally {
			try {
				if (ou != null) ou.close();
			} catch (Exception ex) {
				log.log(Level.WARNING, "error closing stream", ex);
			}
		}
	}

	/**
	 * Возвращает список хостов.
	 * 
	 * @return карта хостов, ключ - Integer IP-адрес, значение -
	 *         Collection<String> имена хоста.
	 * @throws IOException
	 *             ошибка чтения файла.
	 */
	public synchronized HostsEntry[] list() throws IOException {
		return getData().values().toArray(new HostsEntry[0]);
	}

	/**
	 * Устанавливает имена хоста.
	 * 
	 * @param ip
	 *            IP-адрес.
	 * @param names
	 *            Collection<String> имена хоста.
	 * @throws IOException
	 */
	public synchronized void set(int ip, String[] names) throws IOException {
		if (names == null) {
			throw new IllegalArgumentException("null names");
		}
		if (names.length < 1) {
			throw new IllegalArgumentException("empty names");
		}
		for (String name : names) {
			if (name == null) {
				throw new IllegalArgumentException("null name in list");
			}
		}
		Map<Integer, HostsEntry> data = getData();
		data.put(Integer.valueOf(ip), new HostsEntry(ip, names));
		saveData(data);
	}

	/**
	 * Удаляет хост из списка.
	 * 
	 * @param ip
	 *            IP-адрес.
	 * @throws IOException
	 *             ошибка ввода/вывода файла.
	 */
	public synchronized void remove(int ip) throws IOException {
		Map<Integer, HostsEntry> data = getData();
		data.remove(Integer.valueOf(ip));
		saveData(data);
	}
}
